ダウンロードボタンを作成する
アプリには、長時間実行される動作を実行するボタンがたくさんあります。 たとえば、ボタンによってダウンロードがトリガーされる場合があります。 ダウンロードプロセスを開始し、時間の経過とともにデータを受信します。 そして、ダウンロードされたアセットへのアクセスを提供します。 ユーザーに進捗状況を示すと便利です 実行時間が長いプロセスなので、ボタン自体が適切な場所です このフィードバックを提供するために。このレシピでは、 次のように遷移するダウンロード ボタンを作成します。 アプリのダウンロードのステータスに基づいて、複数の視覚的な状態を表示します。
次のアニメーションはアプリの動作を示しています。
新しいステートレス ウィジェットを定義する
ボタン ウィジェットは、時間の経過とともに外観を変更する必要があります。 したがって、ボタンをカスタムで実装する必要があります。 ステートレスなウィジェット。
という新しいステートレス ウィジェットを定義します。DownloadButton
。
@immutable
class DownloadButton extends StatelessWidget {
const DownloadButton({
super.key,
});
@override
Widget build(BuildContext context) {
// TODO:
return const SizedBox();
}
}
ボタンの可能な表示状態を定義する
ダウンロード ボタンの視覚的な表現は、
与えられたダウンロードステータス。考えられる状態を定義する
ダウンロードしてからアップデートしてくださいDownloadButton
受け入れるために
あるDownloadStatus
そしてDuration
ボタンの長さ
あるステータスから別のステータスにアニメーション化するには時間がかかります。
enum DownloadStatus {
notDownloaded,
fetchingDownload,
downloading,
downloaded,
}
@immutable
class DownloadButton extends StatelessWidget {
const DownloadButton({
super.key,
required this.status,
this.transitionDuration = const Duration(
milliseconds: 500,
),
});
final DownloadStatus status;
final Duration transitionDuration;
@override
Widget build(BuildContext context) {
// TODO: We'll add more to this later.
return const SizedBox();
}
}
ボタンの形状を表示する
ダウンロードボタンはダウンロードに応じて形状が変わります
スターテス。実行中、ボタンには灰色の角の丸い長方形が表示されます。
のnotDownloaded
とdownloaded
州。
ボタンには透明な円が表示されます。fetchingDownload
とdownloading
州。
現在に基づいてDownloadStatus
、
を構築するAnimatedContainer
とともにShapeDecoration
丸みを帯びたものを表示します
長方形または円。
シェイプのウィジェット ツリーを別の形式で定義することを検討してください。Stateless
ウィジェットをメインにbuild()
メソッドはシンプルなままであり、追加が可能です
それが続きます。ウィジェットを返す関数を作成する代わりに、
好きWidget _buildSomething() {}
を作成することを常に好みます。StatelessWidget
またはb6869f80-c431-433d-ae7b-d8743eacc98それはよりパフォーマンスが高いです。もっと
これに関する考慮事項は、ドキュメンテーションまたは Flutter の専用ビデオでYouTube チャンネル。
今のところ、AnimatedContainer
子供はただのSizedBox
別のステップで戻ってくるからです。
@immutable
class DownloadButton extends StatelessWidget {
const DownloadButton({
super.key,
required this.status,
this.transitionDuration = const Duration(
milliseconds: 500,
),
});
final DownloadStatus status;
final Duration transitionDuration;
bool get _isDownloading => status == DownloadStatus.downloading;
bool get _isFetching => status == DownloadStatus.fetchingDownload;
bool get _isDownloaded => status == DownloadStatus.downloaded;
@override
Widget build(BuildContext context) {
return ButtonShapeWidget(
transitionDuration: transitionDuration,
isDownloaded: _isDownloaded,
isDownloading: _isDownloading,
isFetching: _isFetching,
);
}
}
@immutable
class ButtonShapeWidget extends StatelessWidget {
const ButtonShapeWidget({
super.key,
required this.isDownloading,
required this.isDownloaded,
required this.isFetching,
required this.transitionDuration,
});
final bool isDownloading;
final bool isDownloaded;
final bool isFetching;
final Duration transitionDuration;
@override
Widget build(BuildContext context) {
var shape = const ShapeDecoration(
shape: StadiumBorder(),
color: CupertinoColors.lightBackgroundGray,
);
if (isDownloading || isFetching) {
shape = ShapeDecoration(
shape: const CircleBorder(),
color: Colors.white.withOpacity(0),
);
}
return AnimatedContainer(
duration: transitionDuration,
curve: Curves.ease,
width: double.infinity,
decoration: shape,
child: const SizedBox(),
);
}
}
なぜ必要なのか疑問に思うかもしれません。ShapeDecoration
透明な円のウィジェット(非表示であることを考慮)。
目に見えないサークルの目的は調整することです
希望のアニメーション。のAnimatedContainer
丸いものから始まる
矩形。ときDownloadStatus
に変わりますfetchingDownload
、
のAnimatedContainer
角丸長方形からアニメーション化する必要がある
円に変化し、アニメーションが起こるにつれてフェードアウトします。
このアニメーションを実装する唯一の方法は、両方を定義することです。
角丸長方形の開始形状と
円の終わりの形。でも、決勝戦は望まないよね
丸が見えるように透明にすると、
これにより、アニメーションのフェードアウトが発生します。
ボタンのテキストを表示する
のDownloadButton
ディスプレイGET
間にnotDownloaded
段階、OPEN
間にdownloaded
フェーズの間にテキストはありません。
各ダウンロード段階でテキストを表示するウィジェットを追加します。
そしてその間のテキストの不透明度をアニメーション化します。テキストを追加する
の子としてのウィジェット ツリーAnimatedContainer
の中に
ボタンラッパーウィジェット。
@immutable
class ButtonShapeWidget extends StatelessWidget {
const ButtonShapeWidget({
super.key,
required this.isDownloading,
required this.isDownloaded,
required this.isFetching,
required this.transitionDuration,
});
final bool isDownloading;
final bool isDownloaded;
final bool isFetching;
final Duration transitionDuration;
@override
Widget build(BuildContext context) {
var shape = const ShapeDecoration(
shape: StadiumBorder(),
color: CupertinoColors.lightBackgroundGray,
);
if (isDownloading || isFetching) {
shape = ShapeDecoration(
shape: const CircleBorder(),
color: Colors.white.withOpacity(0),
);
}
return AnimatedContainer(
duration: transitionDuration,
curve: Curves.ease,
width: double.infinity,
decoration: shape,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: AnimatedOpacity(
duration: transitionDuration,
opacity: isDownloading || isFetching ? 0.0 : 1.0,
curve: Curves.ease,
child: Text(
isDownloaded ? 'OPEN' : 'GET',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
color: CupertinoColors.activeBlue,
),
),
),
),
);
}
}
ダウンロードの取得中にスピナーを表示する
間にfetchingDownload
フェーズ、DownloadButton
放射状スピナーを表示します。このスピナーはからフェードインします
のnotDownloaded
フェーズとフェードアウト
のfetchingDownload
段階。
ボタンの上にある放射状スピナーを実装します。 形状を変化させ、適切なタイミングでフェードインおよびフェードアウトします。
を削除しました。ButtonShapeWidget
のコンストラクターを使用して、
その構築方法とStack
追加したウィジェット。
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onPressed,
child: Stack(
children: [
ButtonShapeWidget(
transitionDuration: transitionDuration,
isDownloaded: _isDownloaded,
isDownloading: _isDownloading,
isFetching: _isFetching,
),
Positioned.fill(
child: AnimatedOpacity(
duration: transitionDuration,
opacity: _isDownloading || _isFetching ? 1.0 : 0.0,
curve: Curves.ease,
child: ProgressIndicatorWidget(
downloadProgress: downloadProgress,
isDownloading: _isDownloading,
isFetching: _isFetching,
),
),
),
],
),
);
}
ダウンロード中に進行状況と停止ボタンを表示します
後にfetchingDownload
位相はdownloading
段階。
間にdownloading
フェーズ、DownloadButton
放射状進行スピナーを成長スピナーに置き換えます。
放射状の進行状況バー。のDownloadButton
停留所も表示されます
ボタン アイコンを使用すると、ユーザーは進行中のダウンロードをキャンセルできます。
進行状況プロパティをDownloadButton
ウィジェット、
その後、進行状況表示を更新して放射状に切り替えます。
進行中の進行状況バーdownloading
段階。
次に、停止ボタンのアイコンを中央に追加します。 放射状の進行状況バー。
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onPressed,
child: Stack(
children: [
ButtonShapeWidget(
transitionDuration: transitionDuration,
isDownloaded: _isDownloaded,
isDownloading: _isDownloading,
isFetching: _isFetching,
),
Positioned.fill(
child: AnimatedOpacity(
duration: transitionDuration,
opacity: _isDownloading || _isFetching ? 1.0 : 0.0,
curve: Curves.ease,
child: Stack(
alignment: Alignment.center,
children: [
ProgressIndicatorWidget(
downloadProgress: downloadProgress,
isDownloading: _isDownloading,
isFetching: _isFetching,
),
if (_isDownloading)
const Icon(
Icons.stop,
size: 14.0,
color: CupertinoColors.activeBlue,
),
],
),
),
),
],
),
);
}
ボタンタップのコールバックを追加する
あなたの最後の詳細は、DownloadButton
ニーズは
ボタンの動作。ボタンは、ユーザーがタップしたときに何らかの処理を実行する必要があります。
ダウンロードを開始するためのコールバックのウィジェット プロパティを追加します。 ダウンロードをキャンセルして、ダウンロードを開きます。
最後に包みますDownloadButton
の既存のウィジェット ツリー
とともにGestureDetector
ウィジェットを転送し、
対応するコールバック プロパティにイベントをタップします。
@immutable
class DownloadButton extends StatelessWidget {
const DownloadButton({
super.key,
required this.status,
this.downloadProgress = 0,
required this.onDownload,
required this.onCancel,
required this.onOpen,
this.transitionDuration = const Duration(milliseconds: 500),
});
final DownloadStatus status;
final double downloadProgress;
final VoidCallback onDownload;
final VoidCallback onCancel;
final VoidCallback onOpen;
final Duration transitionDuration;
bool get _isDownloading => status == DownloadStatus.downloading;
bool get _isFetching => status == DownloadStatus.fetchingDownload;
bool get _isDownloaded => status == DownloadStatus.downloaded;
void _onPressed() {
switch (status) {
case DownloadStatus.notDownloaded:
onDownload();
break;
case DownloadStatus.fetchingDownload:
// do nothing.
break;
case DownloadStatus.downloading:
onCancel();
break;
case DownloadStatus.downloaded:
onOpen();
break;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onPressed,
child: const Stack(
children: [
/* ButtonShapeWidget and progress indicator */
],
),
);
}
}
おめでとう!表示を変更するボタンがあります ボタンがどのフェーズにあるかに応じて、ダウンロードされていない、 ダウンロードを取得し、ダウンロードし、ダウンロードしました。 これで、ユーザーはタップしてダウンロードを開始したり、タップしてダウンロードをキャンセルしたりできるようになりました。 ダウンロードが進行中の場合、タップして完了したダウンロードを開きます。
インタラクティブな例
アプリを実行します。
- クリック得るを開始するボタン 模擬ダウンロード。
- ボタンが進行状況インジケーターに変わります 進行中のダウンロードをシミュレートします。
- シミュレートされたダウンロードが完了すると、 ボタンはに遷移します開ける、示すために アプリがユーザーに対して準備ができていること ダウンロードしたアセットを開きます。
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
const MaterialApp(
home: ExampleCupertinoDownloadButton(),
debugShowCheckedModeBanner: false,
),
);
}
@immutable
class ExampleCupertinoDownloadButton extends StatefulWidget {
const ExampleCupertinoDownloadButton({super.key});
@override
State<ExampleCupertinoDownloadButton> createState() =>
_ExampleCupertinoDownloadButtonState();
}
class _ExampleCupertinoDownloadButtonState
extends State<ExampleCupertinoDownloadButton> {
late final List<DownloadController> _downloadControllers;
@override
void initState() {
super.initState();
_downloadControllers = List<DownloadController>.generate(
20,
(index) => SimulatedDownloadController(onOpenDownload: () {
_openDownload(index);
}),
);
}
void _openDownload(int index) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Open App ${index + 1}'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Apps')),
body: ListView.separated(
itemCount: _downloadControllers.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: _buildListItem,
),
);
}
Widget _buildListItem(BuildContext context, int index) {
final theme = Theme.of(context);
final downloadController = _downloadControllers[index];
return ListTile(
leading: const DemoAppIcon(),
title: Text(
'App ${index + 1}',
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleLarge,
),
subtitle: Text(
'Lorem ipsum dolor #${index + 1}',
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
),
trailing: SizedBox(
width: 96,
child: AnimatedBuilder(
animation: downloadController,
builder: (context, child) {
return DownloadButton(
status: downloadController.downloadStatus,
downloadProgress: downloadController.progress,
onDownload: downloadController.startDownload,
onCancel: downloadController.stopDownload,
onOpen: downloadController.openDownload,
);
},
),
),
);
}
}
@immutable
class DemoAppIcon extends StatelessWidget {
const DemoAppIcon({super.key});
@override
Widget build(BuildContext context) {
return const AspectRatio(
aspectRatio: 1,
child: FittedBox(
child: SizedBox(
width: 80,
height: 80,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red, Colors.blue],
),
borderRadius: BorderRadius.all(Radius.circular(20)),
),
child: Center(
child: Icon(
Icons.ac_unit,
color: Colors.white,
size: 40,
),
),
),
),
),
);
}
}
enum DownloadStatus {
notDownloaded,
fetchingDownload,
downloading,
downloaded,
}
abstract class DownloadController implements ChangeNotifier {
DownloadStatus get downloadStatus;
double get progress;
void startDownload();
void stopDownload();
void openDownload();
}
class SimulatedDownloadController extends DownloadController
with ChangeNotifier {
SimulatedDownloadController({
DownloadStatus downloadStatus = DownloadStatus.notDownloaded,
double progress = 0.0,
required VoidCallback onOpenDownload,
}) : _downloadStatus = downloadStatus,
_progress = progress,
_onOpenDownload = onOpenDownload;
DownloadStatus _downloadStatus;
@override
DownloadStatus get downloadStatus => _downloadStatus;
double _progress;
@override
double get progress => _progress;
final VoidCallback _onOpenDownload;
bool _isDownloading = false;
@override
void startDownload() {
if (downloadStatus == DownloadStatus.notDownloaded) {
_doSimulatedDownload();
}
}
@override
void stopDownload() {
if (_isDownloading) {
_isDownloading = false;
_downloadStatus = DownloadStatus.notDownloaded;
_progress = 0.0;
notifyListeners();
}
}
@override
void openDownload() {
if (downloadStatus == DownloadStatus.downloaded) {
_onOpenDownload();
}
}
Future<void> _doSimulatedDownload() async {
_isDownloading = true;
_downloadStatus = DownloadStatus.fetchingDownload;
notifyListeners();
// Wait a second to simulate fetch time.
await Future<void>.delayed(const Duration(seconds: 1));
// If the user chose to cancel the download, stop the simulation.
if (!_isDownloading) {
return;
}
// Shift to the downloading phase.
_downloadStatus = DownloadStatus.downloading;
notifyListeners();
const downloadProgressStops = [0.0, 0.15, 0.45, 0.8, 1.0];
for (final stop in downloadProgressStops) {
// Wait a second to simulate varying download speeds.
await Future<void>.delayed(const Duration(seconds: 1));
// If the user chose to cancel the download, stop the simulation.
if (!_isDownloading) {
return;
}
// Update the download progress.
_progress = stop;
notifyListeners();
}
// Wait a second to simulate a final delay.
await Future<void>.delayed(const Duration(seconds: 1));
// If the user chose to cancel the download, stop the simulation.
if (!_isDownloading) {
return;
}
// Shift to the downloaded state, completing the simulation.
_downloadStatus = DownloadStatus.downloaded;
_isDownloading = false;
notifyListeners();
}
}
@immutable
class DownloadButton extends StatelessWidget {
const DownloadButton({
super.key,
required this.status,
this.downloadProgress = 0.0,
required this.onDownload,
required this.onCancel,
required this.onOpen,
this.transitionDuration = const Duration(milliseconds: 500),
});
final DownloadStatus status;
final double downloadProgress;
final VoidCallback onDownload;
final VoidCallback onCancel;
final VoidCallback onOpen;
final Duration transitionDuration;
bool get _isDownloading => status == DownloadStatus.downloading;
bool get _isFetching => status == DownloadStatus.fetchingDownload;
bool get _isDownloaded => status == DownloadStatus.downloaded;
void _onPressed() {
switch (status) {
case DownloadStatus.notDownloaded:
onDownload();
break;
case DownloadStatus.fetchingDownload:
// do nothing.
break;
case DownloadStatus.downloading:
onCancel();
break;
case DownloadStatus.downloaded:
onOpen();
break;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onPressed,
child: Stack(
children: [
ButtonShapeWidget(
transitionDuration: transitionDuration,
isDownloaded: _isDownloaded,
isDownloading: _isDownloading,
isFetching: _isFetching,
),
Positioned.fill(
child: AnimatedOpacity(
duration: transitionDuration,
opacity: _isDownloading || _isFetching ? 1.0 : 0.0,
curve: Curves.ease,
child: Stack(
alignment: Alignment.center,
children: [
ProgressIndicatorWidget(
downloadProgress: downloadProgress,
isDownloading: _isDownloading,
isFetching: _isFetching,
),
if (_isDownloading)
const Icon(
Icons.stop,
size: 14,
color: CupertinoColors.activeBlue,
),
],
),
),
),
],
),
);
}
}
@immutable
class ButtonShapeWidget extends StatelessWidget {
const ButtonShapeWidget({
super.key,
required this.isDownloading,
required this.isDownloaded,
required this.isFetching,
required this.transitionDuration,
});
final bool isDownloading;
final bool isDownloaded;
final bool isFetching;
final Duration transitionDuration;
@override
Widget build(BuildContext context) {
var shape = const ShapeDecoration(
shape: StadiumBorder(),
color: CupertinoColors.lightBackgroundGray,
);
if (isDownloading || isFetching) {
shape = ShapeDecoration(
shape: const CircleBorder(),
color: Colors.white.withOpacity(0),
);
}
return AnimatedContainer(
duration: transitionDuration,
curve: Curves.ease,
width: double.infinity,
decoration: shape,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: AnimatedOpacity(
duration: transitionDuration,
opacity: isDownloading || isFetching ? 0.0 : 1.0,
curve: Curves.ease,
child: Text(
isDownloaded ? 'OPEN' : 'GET',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
color: CupertinoColors.activeBlue,
),
),
),
),
);
}
}
@immutable
class ProgressIndicatorWidget extends StatelessWidget {
const ProgressIndicatorWidget({
super.key,
required this.downloadProgress,
required this.isDownloading,
required this.isFetching,
});
final double downloadProgress;
final bool isDownloading;
final bool isFetching;
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: downloadProgress),
duration: const Duration(milliseconds: 200),
builder: (context, progress, child) {
return CircularProgressIndicator(
backgroundColor: isDownloading
? CupertinoColors.lightBackgroundGray
: Colors.white.withOpacity(0),
valueColor: AlwaysStoppedAnimation(isFetching
? CupertinoColors.lightBackgroundGray
: CupertinoColors.activeBlue),
strokeWidth: 2,
value: isFetching ? null : progress,
);
},
),
);
}
}